import { GlLink, GlAlert, GlCollapsibleListbox, GlFormInput, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SelectionSummary from 'ee/security_dashboard/components/shared/vulnerability_report/selection_summary.vue';
import eventHub from 'ee/security_dashboard/utils/event_hub';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import toast from '~/vue_shared/plugins/global_toast';
import { VULNERABILITY_STATE_OBJECTS, DISMISSAL_REASONS } from 'ee/vulnerabilities/constants';
import projectVulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_vulnerabilities.query.graphql';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';

const { dismissed, ...VULNERABILITY_STATE_OBJECTS_WITHOUT_DISMISSED } = VULNERABILITY_STATE_OBJECTS;

jest.mock('~/vue_shared/plugins/global_toast');

Vue.use(VueApollo);

describe('Selection Summary component', () => {
  let wrapper;

  const createApolloProvider = (...queries) => {
    return createMockApollo([...queries]);
  };

  const findForm = () => wrapper.find('form');
  const findGlAlert = () => wrapper.findComponent(GlAlert);
  const findStatusListbox = () => wrapper.findByTestId('status-listbox');
  const findDismissalReasonListbox = () => wrapper.findByTestId('dismissal-reason-listbox');
  const findListboxItem = (id) => wrapper.findByTestId(`listbox-item-${id}`);
  const findCommentFormInput = () => wrapper.findComponent(GlFormInput);
  const findCancelButton = () => wrapper.findByTestId('cancel-button');
  const findSubmitButton = () => wrapper.find('[type="submit"]');

  const selectStatus = (status) => {
    return findStatusListbox().vm.$emit('select', status);
  };

  const selectDismissalReason = (reason) => {
    return findDismissalReasonListbox().vm.$emit('select', reason);
  };

  const addComment = (comment) => {
    return findCommentFormInput().vm.$emit('input', comment);
  };

  const submitForm = async ({
    state,
    comment = 'some comment',
    dismissalReason = 'false_positive',
    isDismissalReasonEnabled = true,
  }) => {
    await selectStatus(state);

    if (isDismissalReasonEnabled) {
      if (state === 'dismissed') {
        await selectDismissalReason(dismissalReason);
      }

      await addComment(comment);
    }

    findForm().trigger('submit');
  };

  const createComponent = ({
    selectedVulnerabilities = [],
    apolloProvider,
    vulnerabilitiesQuery,
    vulnerabilitiesCountsQuery,
    dismissalReason = true,
  } = {}) => {
    wrapper = shallowMountExtended(SelectionSummary, {
      apolloProvider,
      stubs: {
        GlAlert,
        GlSprintf,
        GlCollapsibleListbox,
        GlLink,
      },
      propsData: {
        selectedVulnerabilities,
      },
      provide: {
        vulnerabilitiesQuery,
        vulnerabilitiesCountsQuery,
        glFeatures: { dismissalReason },
      },
    });
  };

  describe('with 1 vulnerability selected', () => {
    beforeEach(() => {
      createComponent({ selectedVulnerabilities: [{ id: 'id_0' }] });
    });

    it('renders correctly', () => {
      expect(findForm().text()).toContain('1 Selected');
      expect(findStatusListbox().exists()).toBe(true);
      expect(findDismissalReasonListbox().exists()).toBe(false);
      expect(findCommentFormInput().exists()).toBe(false);
      expect(findCancelButton().exists()).toBe(true);
      expect(findSubmitButton().exists()).toBe(true);
      expect(findSubmitButton().attributes('disabled')).toBeDefined();
    });
  });

  describe('with multiple vulnerabilities selected', () => {
    beforeEach(() => {
      createComponent({ selectedVulnerabilities: [{ id: 'id_0' }, { id: 'id_1' }] });
    });

    it('renders correctly', () => {
      expect(findForm().text()).toContain('2 Selected');
    });
  });

  describe('change status button', () => {
    beforeEach(() => {
      createComponent();
    });

    it.each(Object.keys(VULNERABILITY_STATE_OBJECTS_WITHOUT_DISMISSED))(
      'is enabled only after selecting status %s',
      async (state) => {
        expect(findSubmitButton().attributes('disabled')).toBeDefined();

        await selectStatus(state);

        expect(findSubmitButton().attributes('disabled')).toBeUndefined();
      },
    );

    it('is enabled only after providing dismissal reason and comment after selecting status dismissed', async () => {
      expect(findSubmitButton().attributes('disabled')).toBeDefined();

      await selectStatus('dismissed');

      expect(findSubmitButton().attributes('disabled')).toBeDefined();

      await selectDismissalReason('false_positive');

      expect(findSubmitButton().attributes('disabled')).toBeDefined();

      await addComment('some comment');

      expect(findSubmitButton().attributes('disabled')).toBeUndefined();
    });
  });

  describe('status listbox', () => {
    it('shows the placeholder text when no status is selected', () => {
      createComponent();

      expect(findStatusListbox().props('toggleText')).toBe(
        wrapper.vm.$options.i18n.statusTogglePlaceholder,
      );
    });

    it('shows expected items', () => {
      createComponent();

      const states = Object.values(VULNERABILITY_STATE_OBJECTS);

      expect(findStatusListbox().props('items')).toHaveLength(states.length);

      states.forEach((state) => {
        const itemText = findListboxItem(state.state).text();
        expect(itemText).toContain(state.dropdownText);
        expect(itemText).toContain(state.dropdownDescription);
      });
    });

    it.each(Object.entries(VULNERABILITY_STATE_OBJECTS))(
      'shows the expected text in the listbox button when %s is clicked',
      async (_key, { state, dropdownText }) => {
        createComponent();

        await selectStatus(state);

        expect(findStatusListbox().props('toggleText')).toBe(dropdownText);
      },
    );
  });

  describe('dismissal reason listbox', () => {
    beforeEach(() => {
      createComponent();
    });

    it.each(Object.keys(VULNERABILITY_STATE_OBJECTS_WITHOUT_DISMISSED))(
      'does not render after selecting status %s',
      async (state) => {
        await selectStatus(state);

        expect(findDismissalReasonListbox().exists()).toBe(false);
      },
    );

    it('renders after selecting status dismissed', async () => {
      expect(findDismissalReasonListbox().exists()).toBe(false);

      await selectStatus('dismissed');

      expect(findDismissalReasonListbox().exists()).toBe(true);
    });

    it('shows the placeholder text when no reason is selected', async () => {
      createComponent();

      await selectStatus('dismissed');

      expect(findDismissalReasonListbox().props('toggleText')).toBe(
        wrapper.vm.$options.i18n.dismissalReasonTogglePlaceholder,
      );
    });

    it('passes expected items', async () => {
      await selectStatus('dismissed');

      expect(findDismissalReasonListbox().props('items')).toEqual(
        Object.entries(DISMISSAL_REASONS).map(([reason, text]) => ({
          value: reason,
          text,
        })),
      );
    });

    it.each(Object.entries(DISMISSAL_REASONS))(
      'shows the expected text in the listbox button when %s is clicked',
      async (key, text) => {
        createComponent();

        await selectStatus('dismissed');
        await selectDismissalReason(key);

        expect(findDismissalReasonListbox().props('toggleText')).toBe(text);
      },
    );

    describe('when dismissal_reason feature flag is false', () => {
      beforeEach(() => {
        createComponent({ dismissalReason: false });
      });

      it.each(Object.keys(VULNERABILITY_STATE_OBJECTS))(
        'does not render after selecting status %s',
        async (state) => {
          await selectStatus(state);

          expect(findDismissalReasonListbox().exists()).toBe(false);
        },
      );
    });
  });

  describe('comment input', () => {
    beforeEach(() => {
      createComponent();
    });

    it.each(Object.keys(VULNERABILITY_STATE_OBJECTS))(
      'renders after selecting status %s',
      async (state) => {
        expect(findCommentFormInput().exists()).toBe(false);

        await selectStatus(state);

        expect(findCommentFormInput().exists()).toBe(true);
      },
    );

    it.each(Object.keys(VULNERABILITY_STATE_OBJECTS_WITHOUT_DISMISSED))(
      'passes comment placeholder for status %s',
      async (state) => {
        await selectStatus(state);

        expect(findCommentFormInput().attributes('placeholder')).toBe(
          wrapper.vm.$options.i18n.commentPlaceholder,
        );
      },
    );

    it('passes required comment placeholder for status dismissed', async () => {
      await selectStatus('dismissed');

      expect(findCommentFormInput().attributes('placeholder')).toBe(
        wrapper.vm.$options.i18n.requiredCommentPlaceholder,
      );
    });

    describe('when dismissal_reason feature flag is false', () => {
      beforeEach(() => {
        createComponent({ dismissalReason: false });
      });

      it.each(Object.keys(VULNERABILITY_STATE_OBJECTS))(
        'does not render after selecting status %s',
        async (state) => {
          await selectStatus(state);

          expect(findCommentFormInput().exists()).toBe(false);
        },
      );
    });
  });

  describe.each(Object.entries(VULNERABILITY_STATE_OBJECTS))(
    'state dropdown change - %s',
    (state, { action, payload, mutation }) => {
      const selectedVulnerabilities = [
        { id: 'gid://gitlab/Vulnerability/54', vulnerabilityPath: '/vulnerabilities/54' },
        { id: 'gid://gitlab/Vulnerability/56', vulnerabilityPath: '/vulnerabilities/56' },
        { id: 'gid://gitlab/Vulnerability/58', vulnerabilityPath: '/vulnerabilities/58' },
      ];

      describe('when API call fails', () => {
        beforeEach(() => {
          const apolloProvider = createApolloProvider([
            mutation,
            jest.fn().mockRejectedValue({
              data: {
                [mutation.definitions[0].name.value]: {
                  errors: [
                    {
                      message: 'Something went wrong',
                    },
                  ],
                },
              },
            }),
          ]);

          createComponent({ apolloProvider, selectedVulnerabilities });
        });

        it(`does not emit vulnerability-updated event - ${action}`, async () => {
          await submitForm({ state });
          await waitForPromises();
          expect(wrapper.emitted()['vulnerability-updated']).toBeUndefined();
        });

        it(`shows alert - ${action}`, async () => {
          await submitForm({ state });
          await waitForPromises();

          expect(findGlAlert().text()).toMatchInterpolatedText(
            'Failed updating vulnerabilities with the following IDs: 54, 56, 58',
          );

          const links = wrapper.findAllComponents(GlLink);
          selectedVulnerabilities.forEach(({ vulnerabilityPath }, index) => {
            expect(links.at(index).attributes('href')).toBe(vulnerabilityPath);
          });
        });
      });

      describe('when API call is successful', () => {
        let apolloProvider;

        const requestHandler = jest.fn().mockResolvedValue({
          data: {
            [mutation.definitions[0].name.value]: {
              errors: [],
              vulnerability: {
                id: selectedVulnerabilities[0].id,
                [`${state}At`]: '2020-09-16T11:13:26Z',
                state: state.toUpperCase(),
                ...(state !== 'detected' && {
                  [`${state}By`]: {
                    id: 'gid://gitlab/User/1',
                  },
                }),
                stateTransitions: {
                  nodes: {
                    dismissalReason: state === 'dismissed' ? 'false_positive' : null,
                  },
                },
              },
            },
          },
        });

        beforeEach(() => {
          apolloProvider = createApolloProvider([mutation, requestHandler]);

          createComponent({
            apolloProvider,
            selectedVulnerabilities,
          });
        });

        it(`calls the mutation with the expected data and emits an update for each vulnerability - ${action}`, async () => {
          const mockComment = 'test comment';
          const mockDismissalReason = 'mitigating_control';

          await submitForm({ state, comment: mockComment, dismissalReason: mockDismissalReason });
          await waitForPromises();
          selectedVulnerabilities.forEach((v, i) => {
            expect(wrapper.emitted()['vulnerability-updated'][i][0]).toBe(v.id);

            const mutationPayload = {
              id: v.id,
              comment: mockComment,
            };

            if (state === 'dismissed') {
              mutationPayload.dismissalReason = mockDismissalReason.toUpperCase();
            }

            expect(requestHandler).toHaveBeenCalledWith(expect.objectContaining(mutationPayload));
          });

          expect(requestHandler).toHaveBeenCalledTimes(selectedVulnerabilities.length);
        });

        it('clears the apollo cache to ensure that previously made queries with status filters will be freshly fetched', async () => {
          const cacheClearSpy = jest.spyOn(apolloProvider.defaultClient, 'clearStore');

          expect(cacheClearSpy).not.toHaveBeenCalled();

          await submitForm({ state });
          await waitForPromises();

          expect(cacheClearSpy).toHaveBeenCalledTimes(1);
        });

        describe('when dismissal_reason feature flag is false', () => {
          beforeEach(() => {
            createComponent({
              apolloProvider,
              selectedVulnerabilities,
              dismissalReason: false,
            });
          });

          it(`calls the mutation with the expected data and emits an update for each vulnerability - ${action}`, async () => {
            await submitForm({ state, isDismissalReasonEnabled: false });
            await waitForPromises();
            selectedVulnerabilities.forEach((v, i) => {
              expect(wrapper.emitted()['vulnerability-updated'][i][0]).toBe(v.id);

              expect(requestHandler).toHaveBeenCalledWith(
                expect.objectContaining({ id: v.id, ...payload }),
              );
            });

            expect(requestHandler).toHaveBeenCalledTimes(selectedVulnerabilities.length);
          });
        });

        it(`calls the toaster - ${action}`, async () => {
          await submitForm({ state });
          await waitForPromises();
          // Workaround for the detected state, which shows as "needs triage" in the UI but uses
          // "detected" behind the scenes.
          const stateString =
            state === VULNERABILITY_STATE_OBJECTS.detected.state ? 'needs triage' : state;

          expect(toast).toHaveBeenLastCalledWith(`3 vulnerabilities set to ${stateString}`);
        });

        it(`the buttons are unclickable during form submission - ${action}`, async () => {
          const areElementsDisabled = () => {
            const areGeneralElementDisabled =
              findSubmitButton().props('loading') &&
              findCancelButton().props('disabled') &&
              findStatusListbox().props('disabled') &&
              findCommentFormInput().attributes('disabled') === 'true';

            if (state === 'dismissed') {
              return areGeneralElementDisabled && findDismissalReasonListbox().props('disabled');
            }

            return areGeneralElementDisabled;
          };

          expect(findSubmitButton().props('disabled')).toBeDefined();
          await submitForm({ state });
          await nextTick();
          expect(areElementsDisabled()).toBe(true);
          await waitForPromises();
          expect(areElementsDisabled()).toBe(false);
        });

        it(`emits an event for the event hub - ${action}`, async () => {
          const spy = jest.fn();
          eventHub.$on('vulnerabilities-updated', spy);

          await submitForm({ state });
          await waitForPromises();
          expect(spy).toHaveBeenCalled();
        });
      });
    },
  );

  describe('refetch queries', () => {
    it('uses expected queries with refetchQueries', async () => {
      const selectedVulnerabilities = [{}, {}, {}];

      createComponent({
        apolloProvider: createApolloProvider(),
        selectedVulnerabilities,
        vulnerabilitiesQuery: projectVulnerabilitiesQuery,
        vulnerabilitiesCountsQuery: countsQuery,
      });
      const spy = jest
        .spyOn(wrapper.vm.$apollo, 'mutate')
        .mockResolvedValue({ data: { vulnerabilityDismiss: {} } });
      await submitForm({ state: 'dismissed' });

      expect(spy).toHaveBeenCalledTimes(selectedVulnerabilities.length);
      expect(spy).toHaveBeenCalledWith(
        expect.objectContaining({
          refetchQueries: [projectVulnerabilitiesQuery, countsQuery],
        }),
      );
    });
  });
});
